BlogAboutGuestBook
인스타그램 주소
HJ DevLog
©2025 효중킴의 블로그, Powered By Next.js

탭간 데이터 공유하기

같은 origin에서 탭간 데이터를 서로 공유하는 방법

2025-09-14

채팅상담 시 상담원이 보는 화면을 개발하고 있는 중에 아래와 같은 요구사항이 존재

  • 어느 채팅상담 페이지에 있더라도, 특정 버튼을 클릭 시 상담메인 페이지로 라우팅 후, 고객정보를 새로운 탭UI로 띄운다.
  • 이 때 상담메인 페이지가 브라우저 상에서 여러 탭일 경우, 회원정보가 모든 브라우저에서 동기화되어야 한다.

마크다운 이미지

처음 생각한 방식

해당 방식의 문제점

브라우저의 여러 탭에서 같은 상담메인페이지를 열어둔 경우 사용자가 보고 있는 페이지에서만 동작함.

브라우저의 탭은 최상위 브라우징 컨텍스트로 각 탭은 자체 Document, Window, History, FrameTree, 자바스크립트 글로벌 객체를 가진다.

window.addEventListener는 여러 탭에 걸쳐 동작하지 않는 이유는 브라우저의 기본 보안 모델 때문. 각 탭은 별도의 실행 환경(context)에서 작동하기 때문

브라우저에서 각 탭은 독립된 윈도우 객체를 가지며,

이벤트 리스너는 해당 윈도우 객체에만 바인딩됩니다.

따라서 한 탭에서 등록한 이벤트 리스너는 다른 탭에서는 감지되지 않습니다.

  • 암묵적 공유가 없다: 탭은 서로의 힙에 손댈 수 없다. 공유를 원하면 반드시 저장소나 메시징 같은 “명시적 채널”을 열어야 한다.
  • 세션 단위 상태: sessionStorage, 페이지 내부 상태, window.name 등은 본질적으로 탭 한정.

위에서 언급한 "명시적 채널"을 사용한다면 통신이 가능하며 이는 대표적으로, , 등이 있다.

이 과정에서 BroadCastChannel을 알게 되었고, 해당 API를 이용해서 구현하기로 결정

BroadCastChannel을 통해 상담메인의 여러 탭들간 동기화를 시켜주고자 했음

BroadCastChannel

BroadcastChannel API란 동일한 origin의 브라우징 맥락(창, 탭, 프레임, iframe, …) 간 데이터 통신을 가능하게 하는 기술

new BroadcastChannel('name')로 참가할 수 있으며 channel.postMessage(data)로 모든 참가자에 전달할 수 있다.

하지만, 지원되지 않는 브라우저가 있을 수 있으며 동일 출처만 참가할 수 있다는 단점(높은 보안성을 의미하므로 단점으로 보기는 어려울 수 있음)이 있다.

마크다운 이미지

기본적으로 BroadcastChannel 객체를 생성하고,

생성된 객체에서 postMessage 메서드를 호출하면 해당 채널에 연결된 BroadcastChannel 객체에 전달되게끔 한다.

간략하게 사용법을 정리해보자면,,

그래서 어떻게 구현하였을까?

먼저 싱글톤형태의 클래스를 하나 만들었음.

요 클래스는 브로드캐스트 채널을 사용하여 여러 탭 간에 고객 정보를 공유하는 것이 목적임

  • sendCustomerInfo(): 고객 정보를 다른 탭으로 전송.
  • onReceiveCustomerInfo(): 다른 탭에서 전송된 고객 정보를 받아 콜백함수 실행

이렇게 CustomerInfoChannel이라는 브로드캐스트 채널을 만들고, 실제 채팅페이지에서 상담메인 버튼 클릭 시 요런 로직을 넣어둠

이제 sendCustomerInfo로 전송된 고객정보를 받는 로직을 심어둠.

onReceiveCustomerInfo를 통해, 전송된 고객정보를 받아서 상담메인의 새 탭에 넣는 콜백을 실행

언제 쓰일 수 있을까?

지금과 같은 요구사항 이외에도 충분히 다른 곳에서 활용할 수 있다.

예를 들어서,,,

A 웹뷰 액티비티에서 B 액티비티, 또는 B, C, D 액티비티 전부로 데이터를 보내야 하는 경우가 있을 수 있다.

예를 들어서 ,,

  1. 게시글 리스트에서 게시글 상세 액티비티를 연다.
  2. 게시글 상세 액티비티에서 좋아요를 누른다
  3. 게시글 상세 액티비티를 닫았을 때(뒤로가기), 수정된 정보가 게시글 리스트 액티비티에 반영되어 있다.

마크다운 이미지

추가: 브로드캐스트 채널 기능을 제공하는 라이브러리

GitHub - pubkey/broadcast-channel: :satellite: BroadcastChannel to send data between different browser-tabs or nodejs-processes + LeaderElection over the channels https://pubkey.github.io/broadcast-channel/

요런 추상화된 훅으로 분리해도 좋을듯..

tanstack-query에서도 실험적으로 연구중..

broadcastQueryClient (실험적)

매우 중요: 이 유틸리티는 현재 실험적 단계입니다. 이는 마이너 및 패치 릴리스에서 호환성이 깨지는 변경사항이 발생할 수 있음을 의미합니다. 사용 시 주의하세요. 실험적 단계의 기능을 프로덕션 환경에서 사용하시려면, 예기치 않은 문제를 방지하기 위해 패치 레벨 버전을 고정하는 것을 권장합니다.

broadcastQueryClient는 동일한 출처(origin)를 가진 브라우저 탭/창 간에 queryClient의 상태를 브로드캐스트하고 동기화하는 유틸리티입니다.

설치

이 유틸리티는 별도의 패키지로 제공되며 @tanstack/query-broadcast-client-experimental` 임포트로 사용할 수 있습니다.

사용법

broadcastQueryClient함수를 임포트하고, 귀하의 QueryClient인스턴스를 전달하며, 선택적으로broadcastChannel을 설정할 수 있습니다.

tsx

broadcastQueryClient

이 함수에 QueryClient인스턴스와 선택적으로 broadcastChannel을 전달합니다.

Options

옵션 객체:

기본 옵션은 다음과 같습니다:

https://tanstack.com/query/latest/docs/framework/react/plugins/broadcastQueryClient#broadcastqueryclient

Next js app Route의 캐싱-RequestMemorization

컴포넌트 작성에 대한 고민들 (합성 컴포넌트)

  • BroadCastChannel
  • 그래서 어떻게 구현하였을까?
  • 언제 쓰일 수 있을까?
  • 요런 추상화된 훅으로 분리해도 좋을듯..
  • tanstack-query에서도 실험적으로 연구중..
  • broadcastQueryClient
  • Options
버튼 클릭 시 커스텀한 이벤트를 만들어서 전파를 하고, 해당 이벤트를 감지하면 되지 않을까?
//커스텀한 이벤트를 만들고, 감지하자
//요런 느낌으로 구현하면 될 거 같았음.
const customEvent = new CustomEvent('custom-click', {
    bubbles: true,
    detail: 채팅상담중인사용자의정보
});

//버튼 클릭 시 핸들러
const handleButtonClick = () => {
   dispatchEvent(customeEvent);
}

//최상단에서 이벤트 감지

const 사용자의정보를_받아서_상담메인_탭에_추가하는_함수 = (data:사용자의정보) => {}

useEffect(() => {
   window.addEventListener("custom-click",사용자의정보를_받아서_상담메인_탭에_추가하는_함수))
},[])

로컬 스토리지
BroadcastChannel
postMessage
// 채널 생성 (동일한 이름의 채널끼리 통신 가능)
const channel = new BroadcastChannel("tab_communication");

// 메시지 수신 이벤트 리스너 등록
channel.onmessage = (event) => {
  console.log("메시지 수신:", event.data);
  document.getElementById("received").textContent = event.data;
};

// 메시지 전송 함수
function sendMessage() {
  const message = document.getElementById("message").value;
  channel.postMessage(message);
}

// 채널 연결 종료 (페이지 언로드 시 권장)
function closeChannel() {
  channel.close();
}
import { CustomerTabInfo } from "@/app/(상담)/(상담메인)/c2001/hooks/use-consulting-tabs";

class CustomerInfoChannel {
  //싱글톤형태로 만들기
  static instance: CustomerInfoChannel | null = null;

  channel: BroadcastChannel | null = null;

  constructor() {
    if (CustomerInfoChannel.instance) {
      return CustomerInfoChannel.instance;
    }

    this.channel = new BroadcastChannel("customer-info-channel");
    CustomerInfoChannel.instance = this;
  }

  //고객정보를 브로드캐스트 채널을 통해 전송(내부적으로 채널의 postMessage사용)
  sendCustomerInfo(newTabInfo: CustomerTabInfo) {
    if (this.channel == null) {
      return;
    }

    this.channel.postMessage(newTabInfo);
  }

  //브로드캐스트 채널에서 메시지 수신 시 등록된 콜백 실행
  onReceiveCustomerInfo(callback: (newTabInfo: CustomerTabInfo) => void) {
    if (this.channel == null) {
      return;
    }
    this.channel.onmessage = (event) => {
      callback(event.data);
    };
  }

  static getInstance() {
    if (!CustomerInfoChannel.instance) {
      CustomerInfoChannel.instance = new CustomerInfoChannel();
    }

    return CustomerInfoChannel.instance;
  }
}

export { CustomerInfoChannel };
//버튼 클릭 시 브로드캐스트 채널에서 sendCustomerInfo호출
const handleConsultingMainButton = async (contact) => {
  const customerChannel = CustomerInfoChannel.getInstance();

  if (회원인가) {
    const customerInfo = await searchCustomerInfoByUserId(userId);

    customerChannel.sendCustomerInfo({
      userInfo: {
        phoneNumber: customerInfo?.phoneNumber ?? null,
        clientNo: customerInfo?.clientNo ?? null,
        type: "member",
        isAuthenticated:
          contact.getAttributes()?.["isAuthenticated"]?.value === "Y",
      },
    });

    return;
  }

  window.open("/contacts-main", "managerTab");
};
export function ReceiveAddCustomerEvent({
  children,
}: {
  children: React.ReactNode;
}) {
  const { addUserInConsultantTab } = useConnectHandler();
  const { addNewTabAndActivate } = useConsultingTabs();
  const channel = CustomerInfoChannel.getInstance();

  useEffect(() => {
    //채널에서 고객정보를 받을 때 새 탭으로 추가를 한다.
    channel.onReceiveCustomerInfo((customerData) => {
      addNewTabAndActivate(customerData);
    });
  }, [addNewTabAndActivate, channel]);

  return <>{children}</>;
}
웹뷰의 액티비티란?
-
웹뷰 액티비티는 안드로이드 앱 내에서 웹 콘텐츠를 표시하는 액티비티입니다.
액티비티는 안드로이드에서 사용자 인터페이스를 구성하는 기본 요소로, 하나의 화면을 의미합니다.

웹뷰(WebView)는 앱 내에서 웹 콘텐츠를 표시할 수 있는 컴포넌트이고,
이를 포함한 액티비티를 웹뷰 액티비티라고 합니다 .
import { useEffect, useMemo, useState } from "react";

export function useBroadcastChannel<T>(key: string) {
  const [message, setMessage] = useState<T>();

  const channel = useMemo(() => {
    return new BroadcastChannel(key);
  }, [key]);

  useEffect(() => {
    channel.addEventListener("message", (event) => {
      setMessage(event.data);
    });
  }, [channel]);

  return { message, channel };
}
import { broadcastQueryClient } from "@tanstack/query-broadcast-client-experimental";

const queryClient = new QueryClient();

broadcastQueryClient({
  queryClient,
  broadcastChannel: "my-app",
});
broadcastQueryClient({ queryClient, broadcastChannel });
interface BroadcastQueryClientOptions {
  /** 동기화할 QueryClient */
  queryClient: QueryClient;
  /** 탭과 창 사이에 통신하는 데 사용될
   * 고유한 채널 이름입니다 */
  broadcastChannel?: string;
  /** BroadcastChannel API에 대한 옵션 */
  options?: BroadcastChannelOptions;
}
{
  broadcastChannel = 'tanstack-query',
}